feat(spa): detail-page admin action buttons reusing the changelist actions API (#555)#562
Merged
Merged
Conversation
…tions API (#555) Closes #555. Surface `ModelAdmin.actions` as buttons on the detail page, alongside `History` / `View on site` / `Edit` / `Delete`. Each button calls the **same** changelist action endpoint the list page uses — just with a one-pk array (`[pk]`) — so there's no new wire surface and the existing permission gate / queryset filter / `message_user` / intermediate-redirect-in-new-tab flow all apply unchanged. - Imports: `useList`, `ActionDescriptor` from `@dar/data`. - `DetailResponse` doesn't carry `actions` (they live in the list response — `django-admin-rest-api` owns that wire shape, no change), so DetailPage reads the metadata through `useList({ pageSize: 1 })`. The data layer caches it; for a user who arrived from the list it's essentially free. - `requestDetailAction` + `performDetailAction` mirror the list-page flow: `requires_confirmation` opens the same styled confirm modal (re-reading "Run X on *this object*?"), else runs immediately; `result.redirect` opens in a new tab (the #250 minimum); messages surface as toasts (#442). - Buttons gated by `canChange` — same visibility rule the bulk runner uses on the changelist. Vitest: 145 passed. Typecheck + ESLint (--max-warnings 0) + stylelint + dark-mode guard clean. `pnpm -r build` ok. Closes #555 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez
pushed a commit
that referenced
this pull request
May 28, 2026
Three SPA polish bugs reported against v1.0.1, all addressed in this PR; no API / contract / schema change. #570 — list filter row wrap (FilterBar trailing slot) ----------------------------------------------------- The trailing slot rendered its children inside a sub-wrapper "<div className=ml-auto flex flex-wrap>" — which behaved as one flex item in the outer container. When the filter pills wrapped onto a second line, the trailing wrapper wrapped as a unit and ml-auto pushed it to the right edge of the new (empty) line — producing the visually-separate "second toolbar row" the pilot reported. Fix: render the trailing children as DIRECT siblings of the pills, no sub-wrapper. React.Children.toArray + cloneElement injects ml-auto on the first non-null trailing child to push the cluster right; flex-wrap then keeps the trailing buttons glued to the end of the last pill row, never on a separate line. #571 — per-object actions on detail (single-pk semantics) --------------------------------------------------------- PR #562 (closing #555) wired the detail page to the CHANGELIST actions endpoint with a one-pk array. That was the wrong primitive: bulk-action verbs (selected files, selected items) are list semantics and confused the operator on a single-object page, and the per-object change_actions from django-object-actions (which the API already surfaces as data.object_actions) were visually buried. Fix: drop the changelist-actions rendering from DetailPage entirely. ObjectActionButton (already wired to runObjectAction → POST app/model/pk/action/name/) remains; it is the correct single-pk primitive. Side effect: ~1.4 kB drop in the SPA bundle from removing the unused useList, runAction, confirm-modal, and runner code paths. #572 — detail header layout (title squeezed, toolbar not right-aligned) ----------------------------------------------------------------------- The header used sm:justify-between with no width hint on either side, so the title block + toolbar block split horizontal space roughly 50/50 even when the title needed more and the toolbar would fit in less. The toolbar block had flex flex-wrap but no justify-end, so wrapped button rows were left-aligned within the right column — reading as "centered between title and viewport" rather than "right-aligned to the page". Fix: title block → min-w-0 flex-1 (claims all available width, truncates only when it has to). Toolbar block → shrink-0 flex-wrap justify-end. Toolbar only pushes the title when its content genuinely needs the room, and wrapped button rows hug the right edge. Closes #570, #571, #572. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #555.
Why
The detail page exposed History / Edit / Delete but no admin-action buttons — so an admin's `actions = […]` was reachable from the changelist (bulk) but not from the detail of a single object. A user looking at one record had to go back to the list, single-select, and dispatch from there.
What
Why no API change
`DetailResponse` doesn't carry `actions` (the changelist actions live in the list response — and `django-admin-rest-api` owns the wire shape). DetailPage reads them via `useList({ pageSize: 1 })`; the data layer caches it. For a user who arrived from the list view, the call is already cached and essentially free.
Verification
Tier 4 (frontend only, no backend / contract change).
🤖 Generated with Claude Code